package study.alex.resumedb.gae;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.StoredCredential;
import com.google.api.client.googleapis.auth.oauth2.*;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.store.DataStore;
import com.google.api.client.util.store.DataStoreFactory;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * Credential manager to get, save, delete user credentials.
 *
 * @author jbd@google.com (Burcu Dogan)
 */
public class CredentialManager {

    public static enum GoogleDiskAccessStatus implements Serializable {UNKNOWN, DENY, GRANT};

    private static CredentialManager crInstance;

    /**
     * Client secrets object.
     */
    private GoogleClientSecrets clientSecrets;

    /**
     * Transport layer for OAuth2 client.
     */
    private HttpTransport transport;

    /**
     * JSON factory for OAuth2 client.
     */
    private JsonFactory jsonFactory;

    /**
     * Scopes for which to request access from the user.
     */
    public static final List<String> SCOPES = Arrays.asList(
            // Required to access and manipulate files.
            "https://www.googleapis.com/auth/drive.file",
//            "https://www.googleapis.com/auth/drive.readonly",
            // Required to identify the user in our data store.
            "https://www.googleapis.com/auth/userinfo.email",
            "https://www.googleapis.com/auth/userinfo.profile");

    public static final String DENY_GOOGLE_DISK_DATA_STORE_ID = "DenyGoogleDiskDatastore";

    /**
     * Credential store to get, save, delete user credentials.
     */
    private static DataStore<StoredCredential> credentialStore;

    /**
     * Store for denyGoogleDisk flag
     */
    private static DataStore<GoogleDiskAccessStatus> denyGoogleDiskFlagStore;


    /**
     * Credential Manager constructor.
     * @param clientSecrets App client secrets to be used during OAuth2 exchanges.
     * @param transport Transportation layer for OAuth2 client.
     * @param factory JSON factory for OAuth2 client.
     * @param storeFactory DataStoreFactory for store credential.
     */
    public CredentialManager(GoogleClientSecrets clientSecrets,
                             HttpTransport transport, JsonFactory factory, DataStoreFactory storeFactory) throws IOException {
        this.clientSecrets = clientSecrets;
        this.transport = transport;
        this.jsonFactory = factory;

        credentialStore = storeFactory.getDataStore(StoredCredential.DEFAULT_DATA_STORE_ID);
        denyGoogleDiskFlagStore = storeFactory.getDataStore(DENY_GOOGLE_DISK_DATA_STORE_ID);
        crInstance = this;
    }

    public static CredentialManager getInstance() {
        return crInstance;
    }

    /**
     * Builds an empty credential object.
     * @return An empty credential object.
     */
    public Credential buildEmpty() {
        return new GoogleCredential.Builder()
                .setClientSecrets(this.clientSecrets)
                .setTransport(transport)
                .setJsonFactory(jsonFactory)
                .build();
    }

    /**
     * Returns credentials of the given user, returns null if there are none.
     * @param userId The id of the user.
     * @return A credential object or null.
     */
    public Credential get(String userId) throws IOException {
        Credential credential = buildEmpty();
        StoredCredential st = credentialStore.get(userId);

        if (st != null  ) {
            credential.setAccessToken(st.getAccessToken())
                    .setRefreshToken(st.getRefreshToken())
                    .setExpirationTimeMilliseconds(st.getExpirationTimeMilliseconds());
            return credential;
        }
        return null;
    }

    /**
     * Saves credentials of the given user.
     * @param userId The id of the user.
     * @param credential A credential object to save.
     */
    public void save(String userId, Credential credential) throws IOException {

        StoredCredential st = new StoredCredential(credential);
        credentialStore.set(userId, st);
        saveStatus(userId, GoogleDiskAccessStatus.GRANT);
    }

    /**
     * Deletes credentials of the given user.
     * @param userId The id of the user.
     */
    public void delete(String userId) throws IOException {
        credentialStore.delete(userId);
        deleteStatus(userId);
    }

    /**
     * Generates a consent page url.
     * @return A consent page url string for user redirection.
     */
    public String getAuthorizationUrl(String state) {
        GoogleAuthorizationCodeRequestUrl urlBuilder =
                new GoogleAuthorizationCodeRequestUrl(
                        clientSecrets.getWeb().getClientId(),
                        clientSecrets.getWeb().getRedirectUris().get(0),
                        SCOPES).setAccessType("offline")
                        .setApprovalPrompt("force")
                        .setState(state);
        return urlBuilder.build();
    }

    /**
     * Retrieves a new access token by exchanging the given code with OAuth2
     * end-points.
     * @param code Exchange code.
     * @return A credential object.
     */
    public Credential retrieve(String code) {
        try {
            GoogleTokenResponse response = new GoogleAuthorizationCodeTokenRequest(
                    transport,
                    jsonFactory,
                    clientSecrets.getWeb().getClientId(),
                    clientSecrets.getWeb().getClientSecret(),
                    code,
                    clientSecrets.getWeb().getRedirectUris().get(0)).execute();
            return buildEmpty().setFromTokenResponse(response);
        } catch (IOException e) {
            new RuntimeException("An unknown problem occured while retrieving token");
        }
        return null;
    }

    /**
     * Returns status of access to Google disk of the given user.
     * @param userId The id of the user.
     * @return A status.
     */
    public GoogleDiskAccessStatus getStatus(String userId) throws IOException {

        GoogleDiskAccessStatus status = denyGoogleDiskFlagStore.get(userId);

        return status == null ? GoogleDiskAccessStatus.UNKNOWN : status;
    }

    /**
     * Saves info about access to Google disk for the given user.
     * @param userId The id of the user.
     * @param newStatus A new status for user.
     */
    public void saveStatus(String userId, GoogleDiskAccessStatus newStatus) throws IOException {

        denyGoogleDiskFlagStore.set(userId, newStatus);
    }

    /**
     * Deletes status of the given user.
     * @param userId The id of the user.
     */
    public void deleteStatus(String userId) throws IOException {
        denyGoogleDiskFlagStore.delete(userId);
    }

    public void drop(String userId) throws IOException {
        deleteStatus(userId);
        delete(userId);
    }

    public Set<String> getKeySet() throws IOException {
        return denyGoogleDiskFlagStore.keySet();
    }
}